package chathub

import (
	"context"
	"fmt"
	"mychat/internal/common/errors"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"google.golang.org/protobuf/proto"

	"mychat/api"
	"mychat/internal/common/cmctx"
)

const (
	writeWait      = 10 * time.Second
	pongWait       = 60 * time.Second
	pingPeriod     = (pongWait * 9) / 10
	maxMessageSize = 1024
)

var (
	newline = []byte{'\n'}
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

type chatClient struct {
	hub  *chatHub
	conn *websocket.Conn
	send chan []byte
	ctx  context.Context
	ones sync.Once
}

func (c *chatClient) closeSend() {
	c.ones.Do(func() {
		close(c.send)
	})
}

func (c *chatClient) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.conn.Close()
	}()
	c.conn.SetReadLimit(maxMessageSize)
	_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error {
		c.conn.SetReadDeadline(time.Now().Add(pongWait))
		return nil
	})
	for {
		_, message, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				c.hub.lg.Error(c.ctx, "client conn.ReadMessage failed||err=%v", err)
			}
			break
		}

		var preq api.Request
		err = proto.Unmarshal(message, &preq)
		if err != nil {
			c.hub.lg.Error(c.ctx, "proto.Request unmarshal message failed||err=%v", err)

			// to delete for debug
			message, _ = proto.Marshal(&errors.DefaultError)
			c.send <- message
			time.Sleep(time.Second)

			break
		}

		var respb []byte
		switch preq.OpCode {
		case api.OpCode_OpUserLogout:
			_, _ = c.hub.userSvc.Logout(c.ctx)
			c.hub.unregister <- c
		case api.OpCode_OpFriendApl:
			var req api.ApplyFriendRequest
			proto.Unmarshal(preq.Payload, &req)
			_, err := c.hub.userSvc.ApplyFriend(c.ctx, &req)
			if err != nil {
				respb, _ = proto.Marshal(err)
				c.send <- respb
			} else {
				send := &api.Send{
					FromUserId:   uint32(cmctx.GetCtxUserId(c.ctx)),
					FromUserName: "",
					ToUserId:     req.TargetId,
					ToUserName:   "",
					Message:      req.Greeting,
					CreateTime:   time.Now().Unix(),
				}
				c.hub.BroadCast(api.OpCode_OpFriendApl, send)
			}
		case api.OpCode_OpFriendCfm:
			var req api.ConfirmFriendRequest
			proto.Unmarshal(preq.Payload, &req)
			_, err := c.hub.userSvc.ConfirmFriend(c.ctx, &req)
			if err != nil {
				respb, _ = proto.Marshal(err)
				c.send <- respb
			} else {
				send := &api.Send{
					FromUserId:   uint32(cmctx.GetCtxUserId(c.ctx)),
					FromUserName: "",
					ToUserId:     req.TargetId,
					ToUserName:   "",
					Message:      "",
					CreateTime:   time.Now().Unix(),
				}
				c.hub.BroadCast(api.OpCode_OpFriendCfm, send)
			}
		case api.OpCode_OpFriendDel:
			var req api.DeleteFriendRequest
			proto.Unmarshal(preq.Payload, &req)
			_, err := c.hub.userSvc.DeleteFriend(c.ctx, &req)
			if err != nil {
				respb, _ = proto.Marshal(err)
				c.send <- respb
			}

		case api.OpCode_OpChatNew:
			var req api.NewChatRequest
			proto.Unmarshal(preq.Payload, &req)
			_, err := c.hub.chatSvc.NewChat(c.ctx, &req)
			if err != nil {
				respb, _ = proto.Marshal(err)
				c.send <- respb
			}
		case api.OpCode_OpChatGet:
		case api.OpCode_OpChatLst:
		case api.OpCode_OpChatIvt:
			var req api.InviteFreindRequest
			proto.Unmarshal(preq.Payload, &req)
			resp, err := c.hub.chatSvc.ChatInviteFriend(c.ctx, &req)
			if err != nil {
				respb, _ = proto.Marshal(err)
				c.send <- respb
			} else {
				fromId := resp.FriendId
				fromName := resp.FriendName
				for _, cu := range resp.ChatUsers {
					send := &api.Send{
						FromUserId:   fromId,
						FromUserName: fromName,
						ToUserId:     cu.UserId,
						ToUserName:   cu.Name,
						Message:      fmt.Sprintf("%s has been invited", fromName),
						CreateTime:   time.Now().Unix(),
					}
					c.hub.BroadCast(api.OpCode_OpFriendCfm, send)
				}
			}
		case api.OpCode_OpChatRmv:
			var req api.RemoveFreindRequest
			proto.Unmarshal(preq.Payload, &req)
			resp, err := c.hub.chatSvc.ChatRemoveFriend(c.ctx, &req)
			if err != nil {
				respb, _ = proto.Marshal(err)
				c.send <- respb
			} else {
				fromId := resp.FriendId
				fromName := resp.FriendName
				for _, cu := range resp.ChatUsers {
					send := &api.Send{
						FromUserId:   fromId,
						FromUserName: fromName,
						ToUserId:     cu.UserId,
						ToUserName:   cu.Name,
						Message:      fmt.Sprintf("%s has been removed", fromName),
						CreateTime:   time.Now().Unix(),
					}
					c.hub.BroadCast(api.OpCode_OpFriendCfm, send)
				}
			}
		case api.OpCode_OpSendText:
			var send *api.Send
			proto.Unmarshal(preq.Payload, send)
			c.hub.BroadCast(api.OpCode_OpSendText, send)
		case api.OpCode_OpSendImage:
		case api.OpCode_OpSendFile:
		case api.OpCode_OpSendAudio:
		default:
			message, _ = proto.Marshal(&errors.OpcodeUnknown)
			c.send <- message
		}
	}
}

func (c *chatClient) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()

	for {
		select {
		case message, ok := <-c.send:
			_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				_ = c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				c.hub.lg.Error(c.ctx, "client conn.NextWrite failed||err=%v", err)
				return
			}
			_, err = w.Write(message)
			if err != nil {
				c.hub.lg.Error(c.ctx, "client conn.ReadMessage failed||err=%v", err)
			}

			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
				w.Write(<-c.send)
			}

			if err = w.Close(); err != nil {
				c.hub.lg.Error(c.ctx, "client conn.Write close failed||err=%v", err)
				return
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			err := c.conn.WriteMessage(websocket.PingMessage, nil)
			if err != nil {
				c.hub.lg.Error(c.ctx, "client conn.Write ping message failed||err=%v", err)
				return
			}
		}
	}
}
